/* 
 * Copyright 2008 Lovely Systems GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lovely.gwt.jsonrpc.rebind;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import lovely.gwt.jsonrpc.client.Cancelable;
import lovely.gwt.jsonrpc.client.JSONServiceBase;
import lovely.gwt.jsonrpc.client.RemoteJSONService;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * Generator of JSON RPC wrappers.
 */
public class JSONProxyGenerator extends Generator {

	Set<String> alreadyProcessed;

	SourceWriter wrapperSourceWriter;
	StringWriter debugWriter;
	int debugIndent;
	private final String tabString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";

	public JSONProxyGenerator() {
		alreadyProcessed = new HashSet<String>(0);
	}

	public String generate(TreeLogger logger, GeneratorContext context,
			String typeName) throws UnableToCompleteException {
		if (!alreadyProcessed.contains(typeName)) {
			alreadyProcessed.add(typeName);
		}
		TypeOracle oracle = context.getTypeOracle();
		JClassType wrappedInterface = oracle.findType(typeName);

		if (!wrappedInterface.isAssignableTo(oracle
				.findType(RemoteJSONService.class.getCanonicalName()))) {
			logAndThrow(logger, "Type " + typeName
					+ " should inherit from RemoteJSONService.");
		}

		JClassType wrappedAsyncInterface = oracle.findType(typeName + "Async");
		checkInterfaces(logger, wrappedInterface, wrappedAsyncInterface);
		String wrapperClassName = simpleStubClassName(wrappedAsyncInterface
				.getName());
		wrapperSourceWriter = getSourceWriter(logger, context,
				wrappedAsyncInterface.getPackage().getName(), wrapperClassName,
				wrappedAsyncInterface.getName(), wrappedInterface);

		if (logger.isLoggable(TreeLogger.ALL)) {
			debugWriter = new StringWriter();
		} else {
			debugWriter = null;
		}
		Set<Object> typesToDeserialize = new HashSet<Object>();
		Set<Object> typesToSerialize = new HashSet<Object>();
		if (wrapperSourceWriter != null) {
			indent();
			JMethod[] methods = wrappedAsyncInterface.getMethods();
			for (int i = 0; i < methods.length; i++) {
				try {
					generateMethodWrapper(context, typesToDeserialize,
							typesToSerialize, methods[i], wrappedInterface
									.getMethods()[i].getReturnType());
				} catch (NotFoundException e) {
					throw new UnableToCompleteException();
				}
			}

			Object[] toDeserialize = typesToDeserialize.toArray();
			for (int i = 0; i < toDeserialize.length; i++) {
				generateTypeDeserializer((JType) toDeserialize[i]);
			}

			Object[] toSerialize = typesToSerialize.toArray();
			for (int i = 0; i < toSerialize.length; i++) {
				generateTypeSerializer((JType) toSerialize[i]);
			}
			outdent();
			wrapperSourceWriter.commit(logger);
			if (logger.isLoggable(TreeLogger.ALL)) {
				logger.log(TreeLogger.ALL, debugWriter.toString(), null);
			}
		}
		return wrappedAsyncInterface.getPackage().getName() + "."
				+ wrapperClassName;
	}

	private void checkInterfaces(TreeLogger logger,
			JClassType wrappedInterface, JClassType wrappedAsyncInterface)
			throws UnableToCompleteException {
		if (wrappedAsyncInterface == null) {
			logAndThrow(logger, "Can't find async interface for "
					+ wrappedInterface.getQualifiedSourceName());
		}
		JMethod[] methods = wrappedInterface.getMethods();
		JMethod[] asyncMethods = wrappedAsyncInterface.getMethods();
		if (methods.length != asyncMethods.length) {
			logAndThrow(logger, "Interfaces "
					+ wrappedInterface.getQualifiedSourceName() + " and "
					+ wrappedAsyncInterface.getQualifiedSourceName()
					+ " should have the same number of methods.");
		}
		Map<String, JMethod> fullMethodsMap = createMethodsMap(logger, methods,
				false);
		Map<String, JMethod> fullAsyncMethodsMap = createMethodsMap(logger,
				asyncMethods, true);

		Object[] fullMethodNames = fullMethodsMap.keySet().toArray();
		for (int i = 0; i < fullMethodNames.length; i++) {
			String methodName = (String) fullMethodNames[i];
			if (!fullAsyncMethodsMap.containsKey(methodName)) {
				logAndThrow(logger, "Couldn't find async method for "
						+ getReadableMethodName(((JMethod) fullMethodsMap
								.get(methodName))));
			}
		}
	}

	private Map<String, JMethod> createMethodsMap(TreeLogger logger,
			JMethod[] methods, boolean fromAsyncInterface)
			throws UnableToCompleteException {
		Map<String, JMethod> methodsMap = new HashMap<String, JMethod>();
		for (int i = 0; i < methods.length; i++) {
			JMethod method = methods[i];
			if (fromAsyncInterface
					&& method.getReturnType() != JPrimitiveType.VOID
					&& !method.getReturnType().getQualifiedSourceName().equals(
							Cancelable.class.getCanonicalName())) {
				logAndThrow(logger, "Method: " + getReadableMethodName(method)
						+ " should return void or Cancelable.");
			}
			StringBuilder methodName = new StringBuilder(method.getName());
			JParameter[] params = method.getParameters();
			int paramsCount = params.length - (fromAsyncInterface ? 1 : 0);
			for (int j = 0; j < paramsCount; j++) {
				methodName.append(getSafeTypeName(params[j].getType()));
			}
			if (fromAsyncInterface
					&& (params.length == 0 || !params[params.length - 1]
							.getType().getQualifiedSourceName().equals(
									AsyncCallback.class.getCanonicalName()))) {
				logAndThrow(logger, "Method's " + getReadableMethodName(method)
						+ " last param should have be of type AsyncCallback");
			}
			methodsMap.put(methodName.toString(), method);
		}
		return methodsMap;
	}

	private static String getReadableMethodName(JMethod method) {
		return String.format("%1$s:%2$s", new Object[] {
				method.getEnclosingType().getQualifiedSourceName(),
				method.getReadableDeclaration(true, true, true, true, true) });
	}

	private static void logAndThrow(TreeLogger logger, String errorMessage)
			throws UnableToCompleteException {
		logger.log(TreeLogger.ERROR, errorMessage, null);
		throw new UnableToCompleteException();
	}

	private void generateTypeSerializer(JType type) {
		String shortTypeName = type.getSimpleSourceName();
		println(String.format(
				"private JSONValue serialize_%1$s(%2$s __toserialize__) {",
				new Object[] { getSafeTypeName(type), shortTypeName }));
		indent();
		println("if (__toserialize__ == null) {");
		indent();
		println("return JSONNull.getInstance();");
		outdent();
		println("};");
		if (type.isArray() != null) {
			println("JSONArray __result__ = new JSONArray();");
			println("for (int __i__ = 0; __i__ < __toserialize__.length; __i__++) {");
			indent();
			println(String
					.format(
							"__result__.set(__i__, serialize_%1$s(__toserialize__[__i__]));",
							new Object[] { getSafeTypeName(((JArrayType) type)
									.getComponentType()) }));
			outdent();
			println("};");
		} else {
			println("JSONObject __result__ = new JSONObject();");
			JMethod[] methods = ((JClassType) type).getMethods();
			for (int i = 0; i < methods.length; i++) {
				JMethod method = methods[i];
				if (method.getParameters().length == 0) {
					String propertyName = null;
					String propertyAccessor = null;
					if (method.getName().startsWith("get")) {
						propertyName = method.getName().substring(3);
						propertyAccessor = "get";
					} else if ((method.getName().startsWith("is"))) {
						propertyName = method.getName().substring(2);
						propertyAccessor = "is";
					}
					if (propertyName != null) {
						propertyName = propertyName.substring(0, 1)
								.toLowerCase()
								+ propertyName.substring(1);
						writeParam(null, "__result__", "__toserialize__."
								+ propertyAccessor, "()", new NameTypePair(
								propertyName, method.getReturnType()));
					}
				}
			}
		}
		println("return __result__;");
		outdent();
		println("}");
	}

	private void generateTypeDeserializer(JType type) {
		String shortTypeName = type.getSimpleSourceName();
		println(String.format(
				"private %1$s deserialize_%2$s(JSONValue __deserialized__) {",
				new Object[] { shortTypeName, getSafeTypeName(type) }));
		indent();
		println("if (__deserialized__.isNull() != null) {");
		indent();
		println("return null;");
		outdent();
		println("};");
		if (type.isArray() != null) {
			JType componentType = ((JArrayType) type).getComponentType();
			println("JSONArray __array__ = (JSONArray)__deserialized__;");
			println(String.format(
					"%1$s[] __result__ = new %1$s[__array__.size()];",
					new Object[] { componentType.getSimpleSourceName() }));
			println("for (int __i__ = 0; __i__ < __array__.size(); __i__++) {");
			indent();
			println(String
					.format(
							"__result__[__i__] = deserialize_%1$s(__array__.get(__i__));",
							new Object[] { getSafeTypeName(componentType) }));
			outdent();
			println("};");
		} else {
			println("JSONObject __object__ = (JSONObject)__deserialized__;");
			println(String.format("%1$s __result__ = new %1$s();",
					new Object[] { shortTypeName }));
			println("JSONValue __value__ = null;");
			JMethod[] methods = ((JClassType) type).getMethods();
			for (int i = 0; i < methods.length; i++) {
				JMethod method = methods[i];
				if (method.getParameters().length == 1) {
					if (method.getName().startsWith("set")) {
						String propertyName = method.getName().substring(3);
						println("__value__ = __object__.get(\"" + propertyName
								+ "\");");
						println("if (__value__ == null) {");
						indent();
						println("__value__ = __object__.get(\""
								+ propertyName.substring(0, 1).toLowerCase()
								+ propertyName.substring(1) + "\");");
						outdent();
						println("};");
						println("if (__value__ != null) {");
						indent();
						println(String
								.format(
										"__result__.%1$s(deserialize_%2$s(__value__));",
										new Object[] {
												method.getName(),
												getSafeTypeName(method
														.getParameters()[0]
														.getType()),
												method.getParameters()[0]
														.getType()
														.getSimpleSourceName() }));
						outdent();
						println("};");
					}
				}
			}
		}
		println("return __result__;");
		outdent();
		println("}");
	}

	private void generateMethodWrapper(GeneratorContext context,
			Set<Object> typesToDeserialize, Set<Object> typesToSerialize,
			JMethod asyncMethod, JType returnType) throws NotFoundException {
		boolean returnsCancelable = asyncMethod.getReturnType() != JPrimitiveType.VOID;
		print("public " + (returnsCancelable ? "Cancelable " : "void "));
		print(asyncMethod.getName() + "(");
		JParameter[] originalParams = asyncMethod.getParameters();
		NameTypePair[] params = new NameTypePair[originalParams.length - 1];
		String asyncCallbackName = originalParams[originalParams.length - 1]
				.getName();
		for (int i = 0; i < originalParams.length; i++) {
			if (i > 0) {
				print(", ");
			}
			print(originalParams[i].getType().getSimpleSourceName());
			print(" ");
			print(originalParams[i].getName());
			if (i < originalParams.length - 1) {
				params[i] = new NameTypePair(originalParams[i].getName(),
						originalParams[i].getType());
			}
		}
		println(") {");
		indent();
		println("JSONObject __request__ = new JSONObject();");
		// version
		println("__request__.put(\"version\", new JSONString(\"1.1\"));");
		// method
		println("__request__.put(\"method\", new JSONString(\"" + asyncMethod.getName() + "\"));");
		// parameters (optional)
		if (params.length > 0) {
			println("JSONObject __params__ = new JSONObject();");
			for (int i = 0; i < params.length; i++) {
				writeParam(typesToSerialize, "__params__", "", "", params[i]);
			}
			println("__request__.put(\"params\", __params__);");
		}
		
		println(String.format("if (%1$s instanceof DebugAsyncCallback) {",
				new Object[] { asyncCallbackName }));
		indent();
		println(String.format(
						"((DebugAsyncCallback)%1$s).beforeSend(__request__);",
						new Object[] { asyncCallbackName }));
		outdent();
		println("}");
		println("RequestBuilder __builder__ = createBuilder(\""
				+ asyncMethod.getName() + "\");");
		createDeserializingCallback(context, typesToDeserialize, returnType,
				asyncCallbackName);

		println("try {");
		indent();
		if (returnsCancelable) {
			print("return new CancelableRequest(");
		}
		print("__builder__.sendRequest(__request__.toString(), __callback__)");
		if (returnsCancelable) {
			println(");");
		} else {
			println(";");
		}
		outdent();
		println("} catch(RequestException __exception__) {");
		indent();
		println("callback.onFailure(__exception__);");
		outdent();
		println("};");
		outdent();
		if (returnsCancelable) {
			println("return null;");
		}
		println("}");

	}

	private void createDeserializingCallback(GeneratorContext context,
			Set<Object> typesToDeserialize, JType returnType,
			String asyncCallbackName) throws NotFoundException {
		String returnTypeName = returnType.getSimpleSourceName();

		println("JSONRequestCallback __callback__ = new JSONRequestCallback("
				+ asyncCallbackName + ") {");
		indent();

		// handle Strings
		println("public Object deserialize(String __response__) {");
		indent();
		if (returnTypeName == "void") {
			println("return null;");
		} else {
			println("JSONValue __jsonValue__ = JSONParser.parse(__response__);");
			println("return deserialize(__jsonValue__);");
		}
		outdent();
		println("}");

		// handle JSONValues
		println("public Object deserialize(JSONValue __jsonValue__) {");
		indent();
		if (returnTypeName == "void") {
			println("return null;");
		} else {
			String safeTypeName;
			if (returnType.isPrimitive() != null) {
				safeTypeName = getSafeTypeName(mapPrimitiveToObject(context,
						returnType));
			} else {
				safeTypeName = getSafeTypeName(returnType);
			}
			println("return deserialize_" + safeTypeName + "(__jsonValue__);");

			if (typesToDeserialize != null) {
				traverseAllSubTypes(typesToDeserialize, returnType);
			}
		}
		outdent();
		println("}");
		outdent();
		println("};");
	}

	private JType mapPrimitiveToObject(GeneratorContext context, JType type)
			throws NotFoundException {
		if (type == JPrimitiveType.BOOLEAN) {
			return context.getTypeOracle().getType("java.lang.Boolean");
		}
		if (type == JPrimitiveType.BYTE) {
			return context.getTypeOracle().getType("java.lang.Byte");
		}
		if (type == JPrimitiveType.SHORT) {
			return context.getTypeOracle().getType("java.lang.Short");
		}
		if (type == JPrimitiveType.INT) {
			return context.getTypeOracle().getType("java.lang.Integer");
		}
		if (type == JPrimitiveType.LONG) {
			return context.getTypeOracle().getType("java.lang.Long");
		}
		if (type == JPrimitiveType.FLOAT) {
			return context.getTypeOracle().getType("java.lang.Float");
		}
		if (type == JPrimitiveType.DOUBLE) {
			return context.getTypeOracle().getType("java.lang.Double");
		}
		if (type == JPrimitiveType.CHAR) {
			return context.getTypeOracle().getType("java.lang.Character");
		}
		return (JType) null;
	}

	private boolean isPrimitiveOrStringType(JType type) {
		// TODO: mapping
		return type instanceof JPrimitiveType
				|| type.getQualifiedSourceName().equals("java.lang.Boolean")
				|| type.getQualifiedSourceName().equals("java.lang.Byte")
				|| type.getQualifiedSourceName().equals("java.lang.Short")
				|| type.getQualifiedSourceName().equals("java.lang.Integer")
				|| type.getQualifiedSourceName().equals("java.lang.Long")
				|| type.getQualifiedSourceName().equals("java.lang.Float")
				|| type.getQualifiedSourceName().equals("java.lang.Double")
				|| type.getQualifiedSourceName().equals("java.lang.Character")
				|| type.getQualifiedSourceName().equals("java.lang.String");
	}

	private String getSafeTypeName(JType type) {
		if (type.isArray() != null) {
			return "array_"
					+ getSafeTypeName(((JArrayType) type).getComponentType());
		}
		return type.getQualifiedSourceName().replace('.', '_');
	}

	private void writeParam(Set<Object> typesToSerialize,
			String JSONObjectName, String paramPrefix, String paramSufix,
			NameTypePair param) {
		println(String.format(
				"%1$s.put(\"%2$s\", serialize_%3$s(%4$s%2$s%5$s));",
				new Object[] { JSONObjectName, param.Name,
						getSafeTypeName(param.Type), paramPrefix, paramSufix }));

		if (typesToSerialize != null) {
			traverseAllSubTypes(typesToSerialize, param.Type);
		}
	}

	private boolean traverseAllSubTypes(Set<Object> types, JType type) {
		if (isPrimitiveOrStringType(type)) {
			System.out.println("traversing primitive: " + type);
			return false;
		} else {
			System.out.println("traversing non-primitive: " + type);
			if (types.contains(type))
				return false;
			types.add(type);
			
			if (type.isArray() != null) {
				traverseAllSubTypes(types, ((JArrayType) type)
						.getComponentType());
			}
			
			if (type.isClass() != null) {
				JMethod[] methods = ((JClassType) type).getMethods();
				for (int i = 0; i < methods.length; i++) {
					JMethod method = methods[i];
					traverseAllSubTypes(types, method.getReturnType());
					JParameter[] params = method.getParameters();
					for (int j = 0; j < params.length; j++) {
						JParameter param = params[j];
						traverseAllSubTypes(types, param.getType());
					}
				}
			}
			return true;
		}
	}

	private SourceWriter getSourceWriter(TreeLogger logger,
			GeneratorContext context, String packageName, String className,
			String interfaceName, JClassType wrappedInterface) {
		PrintWriter printWriter = context.tryCreate(logger, packageName,
				className);
		if (printWriter == null) {
			return null;
		}
		ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
				packageName, className);

		JMethod[] methods = wrappedInterface.getMethods();
		for (int i = 0; i < methods.length; i++) {
			String quailifiedSourceName = methods[i].getReturnType()
					.getQualifiedSourceName();
			if (!quailifiedSourceName.equals("void")
					&& !quailifiedSourceName.endsWith("[]"))
				composerFactory.addImport(methods[i].getReturnType()
						.getQualifiedSourceName());
		}

		composerFactory.addImport("lovely.gwt.jsonrpc.client.Cancelable");
		composerFactory.addImport("lovely.gwt.jsonrpc.client.CancelableRequest");
		composerFactory.addImport("lovely.gwt.jsonrpc.client.JSONServiceBase");
		composerFactory.addImport("lovely.gwt.jsonrpc.client.JSONRequestCallback");
		composerFactory.addImport("lovely.gwt.jsonrpc.client.DebugAsyncCallback");
		composerFactory.addImport("com.google.gwt.user.client.rpc.AsyncCallback");
		composerFactory.addImport("com.google.gwt.user.client.rpc.ServiceDefTarget");
		composerFactory.addImport("com.google.gwt.http.client.RequestBuilder");
		composerFactory.addImport("com.google.gwt.http.client.RequestException");
		composerFactory.addImport("com.google.gwt.json.client.JSONParser");
		composerFactory.addImport("com.google.gwt.json.client.JSONValue");
		composerFactory.addImport("com.google.gwt.json.client.JSONArray");
		composerFactory.addImport("com.google.gwt.json.client.JSONBoolean");
		composerFactory.addImport("com.google.gwt.json.client.JSONNull");
		composerFactory.addImport("com.google.gwt.json.client.JSONNumber");
		composerFactory.addImport("com.google.gwt.json.client.JSONObject");
		composerFactory.addImport("com.google.gwt.json.client.JSONString");
		composerFactory.addImport("com.google.gwt.user.client.Window");
		// composerFactory.addImport("lovely.gwt.jsonrpc.client.JSONInteger");

		composerFactory.setSuperclass(JSONServiceBase.class.getName());
		composerFactory.addImplementedInterface(interfaceName);
		composerFactory.addImplementedInterface(ServiceDefTarget.class
				.getSimpleName());

		return composerFactory.createSourceWriter(context, printWriter);
	}

	private void print(String s) {
		wrapperSourceWriter.print(s);
		if (debugWriter != null) {
			debugWriter.append(s);
		}
	}

	private void println(String s) {
		wrapperSourceWriter.println(s);
		if (debugWriter != null) {
			debugWriter.append(s);
			debugWriter.append('\n');
			if (debugIndent > 0)
				debugWriter.append(tabString.substring(0, debugIndent));
		}
	}

	private void indent() {
		wrapperSourceWriter.indent();
		debugIndent++;
	}

	private void outdent() {
		wrapperSourceWriter.outdent();
		debugIndent--;
	}

	private static String simpleStubClassName(String className) {
		return "__" + className + "_impl__";
	}

	private class NameTypePair {
		public final String Name;
		public final JType Type;

		public NameTypePair(String name, JType type) {
			Name = name;
			Type = type;
		}
	}
}
